← Carlos Paparoni  ·  AssemblerCoding
Caso de estudio — Django · producción · desarrollo en solitario

Una clínica dental funciona con esto. Lo construyó un ingeniero con una flota de agentes.

Diecinueve meses, 922 commits, 715 tickets, un solo autor. Un sistema de gestión de clínicas dentales que maneja citas, historia clínica, facturación, laboratorios y expedientes de pacientes para clínicas en América Latina — Django 5.2, PostgreSQL 17 e información médica protegida en producción.

Odontograma — notación FDI (ISO 3950), cuadrante superior derecho, superficies M·O·D·V·L·I
M mesial   O oclusal   D distal   V vestibular   L lingual   I incisal  ·  FULL marcador de diente completo, excluyente de cualquier superficie
922
commits
715
tickets cerrados
17
apps de Django
416
módulos de prueba
77
suites Playwright
3
idiomas

El sistema

Diecisiete apps, un solo expediente clínico

Radiant Dental es un SaaS multi-inquilino: una clínica tiene sucursales, las sucursales tienen especialidades y servicios, y el personal — odontólogos, higienistas, asistentes, recepcionistas, gerentes — se asigna por sucursal. Sobre eso se apoyan las citas y listas de espera, los expedientes de pacientes y sus cuestionarios de salud, la historia clínica, los planes de tratamiento, la facturación y los pagos, la gestión de casos de laboratorio, los programas de lealtad, los prospectos de marketing y una superficie pública de agendamiento en línea.

Lo interesante no es la lista de funcionalidades. Es que todas esas superficies tocan el mismo expediente del paciente, y ese expediente es información médica protegida.

Modelado de dominio

La parte difícil nunca fue el CRUD

Un diente no es un registro. Los dientes se numeran con el estándar FDI de dos dígitos, y una condición aplica a un conjunto de superficies del diente — mesial, oclusal, distal, vestibular, lingual, incisal — o al diente completo, que es mutuamente excluyente con el resto. El modelo lo almacena como una cadena validada: cada carácter es un código de superficie, sin repeticiones, o el literal FULL.

Un odontograma es una instantánea, no un estado actual. Varios odontogramas por paciente permiten comparar en el tiempo, y un odontograma puede bloquearse con un motivo registrado, porque un registro clínico firmado debe dejar de cambiar. El periodontograma agrega seis sitios de medición por diente; los ítems del plan de tratamiento llevan códigos de procedimiento CDT.

Odontograma de dentición permanente: los cuatro cuadrantes numerados en notación FDI, con marcadores de condición en los dientes 11, 16, 26, 36 y 46.
Superficie de registro clínico, con datos de demostración. Cada diente lleva su número FDI — primero el cuadrante, luego la posición, de modo que 46 es el primer molar inferior derecho. Los marcadores codifican la categoría de la condición, y el 36 se dibuja como contorno porque un diente ausente es un hecho distinto de uno tratado. Preparar esta captura destapó un defecto: cada etiqueta de cuadrante nombraba el cuadrante opuesto, y además estaba escrita en español directamente en el código. Corregido antes de publicar.
Decisión de diseño

Los conflictos de agenda son advertencias, no errores. El verificador de conflictos ejecuta siete comprobaciones independientes — feriados de la sucursal, horario de atención, agenda del profesional, permisos aprobados, bloqueos personales, citas superpuestas y duración del servicio — y devuelve una lista de advertencias categorizadas en lugar de rechazar la cita.

Las recepcionistas sobreagendan a propósito. Un sistema que se los impide termina esquivado en una semana; uno que les dice exactamente qué están pasando por alto termina usado.

Criterio

El documento de diseño que dice "todavía no construyas esto"

El registro clínico por voz — que el odontólogo dicte las mediciones en vez de escribirlas — es la funcionalidad que el producto más obviamente pide. Está especificada en tres documentos de diseño dentro del repositorio, y nada de eso está en INSTALLED_APPS.

La razón está en la especificación. La salida de voz es provisional hasta que un profesional la firma, así que la capa de captura es una app separada que escribe hacia los modelos clínicos solo al confirmar. El audio y las transcripciones nunca cuelgan del expediente clínico: eso mezclaría la procedencia con el registro y obligaría a cada consulta clínica a esquivarla. Los límites de bloqueo y borrador que ya existen se convierten en el destino de la confirmación, en vez de reinventarse.

Escribir eso antes de escribir código es lo que evita que una base de código asistida por IA acumule estructura plausible. También es la razón por la que la funcionalidad no está construida a medias.

Seguridad

Hice una revisión de caja blanca contra mi propio producto

En mayo de 2026 realicé una revisión de seguridad a nivel de código fuente, con alcance en configuración y secretos, autenticación y control de acceso, inyección y manejo de entradas, y protección de información médica. Trece hallazgos, calificados para un sistema con obligaciones tipo HIPAA, cada uno rastreado a código específico y contrastado contra el ruteo de URLs para separar los problemas realmente alcanzables de los inseguros pero no enrutados.

Después los corregí. La concentración estaba exactamente donde uno esperaría: la superficie pública sin autenticar y el servido de archivos estáticos.

ÁreaCorrecciónEstado
Archivos de pacientesTodo el contenido con información médica pasó a servirse tras vistas autenticadas; almacenamiento de objetos conmutable por entorno, con alias privados y públicos separadosCerrado
Registros clínicosAcceso a la historia clínica acotado por clínica; auditoría de acceso a información médica en las superficies de expediente y citasCerrado
Agendamiento públicoLa API pública de citas insegura se eliminó en vez de parchearse; redirecciones de login protegidasCerrado
Carga de archivosValidadores de subida y redacción de datos sensibles en logsCerrado
Superficie del navegadorContent-Security-Policy aplicada con django-csp; orígenes de bucket permitidos solo cuando el almacenamiento de objetos está activoCerrado
Formularios públicosHoneypot y tiempo mínimo de llenado contra bots; registro de la IP real del cliente detrás del proxy; log de intentos fallidosCerrado

Dos cosas que me gustaría que un revisor se lleve de aquí. Los viewsets inseguros sin enrutar se eliminaron, no se corrigieron — código muerto que habría sido peligroso el día que alguien lo conectara. Y la corrección incluye una prueba que verifica que el router público exponga únicamente los endpoints previstos, porque la falla nunca estuvo en el código sino en el ruteo.

Método

Qué hace que el código construido con agentes se sostenga

Cada commit de este repositorio es mío, y la mayor parte del código se escribió con agentes de IA. Con 922 commits eso solo funciona con andamiaje, y el andamiaje es la habilidad real.

Un archivo de convenciones que los agentes sí respetan

El CLAUDE.md del repositorio abre con la única regla que rompe todo si se viola: los comandos de Django se ejecutan dentro de Docker, nunca en el host. No son preferencias de estilo — es la restricción que produce fallas silenciosas y difíciles de diagnosticar.

Subagentes hechos a medida

Un revisor de código, un arquitecto de dominio dental y un arquitecto Django/React, cada uno con su propio encargo. Revisar el dominio e implementar son trabajos distintos y reciben agentes distintos.

Una capa de servicios, exigida

Diez apps tienen un paquete services/. La lógica de negocio vive ahí, no en los modelos ni en las vistas — el límite que evita que el código generado se desparrame a la capa equivocada.

Las pruebas como criterio de aceptación

416 módulos de prueba más 77 suites end-to-end de Playwright, con datos sembrados y un subconjunto de ruta crítica. El código generado es barato; la suite es lo que hace seguro aceptarlo.

Diseño documentado antes de implementar

Especificaciones escritas, contrastadas contra los modelos existentes y marcadas explícitamente como aún no construidas. La capa de registro por voz tiene tres y cero migraciones.

Una pasada adversarial sobre mi propio trabajo

La revisión de seguridad existe porque el código que uno no tecleó a mano merece más escrutinio, no menos. Trece hallazgos sobre una base de código propia son el argumento a favor de la práctica.

Stack

Sobre qué corre

BackendDjango 5.2, Django REST Framework 3.15, drf-spectacular para OpenAPI
DatosPostgreSQL 17, llaves primarias UUID, borrado lógico con espejo de auditoría
AsíncronoCelery + Redis — recordatorios, recitas, solicitudes de reseña, sincronización de calendario
AutenticaciónSesiones de Django, tokens Knox, acceso sin contraseña para pacientes por enlace mágico y OTP por SMS, bloqueo con django-axes, SSO de Google para el personal
FrontendStimulus.js + TypeScript como mejora progresiva sobre plantillas de Django; Jest
IntegracionesGoogle Calendar, SMS por Twilio, WhatsApp Business API y un puente Baileys de WhatsApp Web corriendo como su propio servicio Node
OperaciónDocker Compose, nginx, respaldos nocturnos del volumen de archivos con rotación, PgHero, Sentry con depurador de datos sensibles
IdiomasEspañol, inglés y portugués de Brasil

En resumen

Qué demuestra esto

  • Django a profundidad de producción — no una demo CRUD: multi-inquilino, un expediente clínico con semántica de bloqueo, detección de conflictos de agenda por siete vías, manejo y auditoría de información médica.
  • El modelado de dominio como el trabajo real — notación FDI, validación por superficie, seis sitios de medición periodontal, códigos CDT. El esquema tenía que estar bien antes que cualquier otra cosa.
  • Entrega asistida por IA con las barreras que la hacen creíble — un archivo de convenciones, subagentes a medida, una capa de servicios exigida, 493 módulos y suites de prueba, y especificaciones escritas antes del código.
  • El criterio para dejar cosas sin construir — y para auditar, y borrar, mi propio trabajo.
← Carlos Paparoni  ·  AssemblerCoding
Case study — Django · production · solo build

A dental practice runs on this. One engineer and a fleet of agents built it.

Nineteen months, 922 commits, 715 tickets, one author. A practice-management system handling appointments, clinical charting, billing, labs and patient records for dental clinics in Latin America — Django 5.2, PostgreSQL 17, and protected health information in production.

Odontogram — FDI (ISO 3950) upper right quadrant, surfaces M·O·D·V·L·I
M mesial   O occlusal   D distal   V vestibular   L lingual   I incisal  ·  FULL whole-tooth marker, exclusive of any surface
922
commits
715
tickets closed
17
Django apps
416
test modules
77
Playwright suites
3
locales

The system

Seventeen apps, one clinical record

Radiant Dental is a multi-tenant SaaS: a clinic owns branches, branches own specialties and services, and staff — dentists, hygienists, assistants, receptionists, office managers — are assigned per branch. On top of that sit appointments and waitlists, patient records and health questionnaires, clinical charting, treatment plans, invoicing and payments, lab-case management, loyalty programs, marketing leads, and a public online-booking surface.

The interesting part is not the feature list. It is that every one of those surfaces touches the same patient record, and that record is protected health information.

Domain modelling

The hard part was never the CRUD

A tooth is not a row. Teeth are numbered by the FDI two-digit standard, and a condition applies to a set of surfaces on a tooth — mesial, occlusal, distal, vestibular, lingual, incisal — or to the whole tooth, which is mutually exclusive with the rest. The model stores that as a validated string: each character a surface code, no duplicates, or the literal FULL.

An odontogram is a snapshot, not current state. Multiple charts per patient enable comparison over time, and a chart can be locked with a recorded reason, because a signed clinical record must stop changing. Periodontal charting adds six measurement sites per tooth; treatment plan items carry CDT procedure codes.

Permanent-dentition odontogram: all four quadrants numbered in FDI notation, with condition markers on teeth 11, 16, 26, 36 and 46.
Charting surface, seeded demo data. Every tooth carries its FDI number — quadrant first, then position, so 46 is the lower-left first molar. Markers encode condition category, and 36 is drawn as an outline because a missing tooth is a different kind of fact from a treated one. Preparing this capture surfaced a defect: every quadrant label named the opposite quadrant, and was hardcoded in Spanish besides. Fixed before publishing.
Design decision

Scheduling conflicts are warnings, not errors. The conflict checker runs seven independent checks — branch holiday, operating hours, staff schedule, approved time off, personal blocks, overlapping appointments, and service-duration mismatch — and returns a list of categorised warnings rather than refusing the booking.

Receptionists double-book on purpose. A system that blocks them gets worked around within a week; one that tells them exactly what they are overriding gets used.

Judgment

The design document that says "do not build this yet"

Voice charting — a clinician calling out measurements instead of typing them — is the feature the product most obviously wants. It is specified across three design documents in the repository, and none of it is in INSTALLED_APPS.

The reason is in the spec. Voice output is provisional until a clinician signs it off, so the capture layer is a separate staging app that writes through to the clinical models only on commit. Audio blobs and transcripts never hang off the clinical record — that would mix provenance into the chart and force every clinical query to step around it. The existing lock and draft boundaries become the commit target rather than being reinvented.

Writing that down before writing code is what stops an AI-assisted codebase from accreting plausible structure. It is also why the feature is not half-built.

Security

I ran a white-box review against my own product

In May 2026 I performed a source-level security review of the system, scoped to configuration and secrets, authentication and access control, injection and input handling, and PHI protection. Thirteen findings, rated for a system under HIPAA-style obligations, each traced to specific code and cross-checked against URL routing to separate genuinely reachable issues from insecure-but-unrouted ones.

Then I fixed them. The concentration was exactly where you would expect: the unauthenticated public surface and static-file serving.

AreaRemediationState
Patient mediaAll PHI media moved behind authenticated views; env-toggled object storage with private PHI and public asset aliasesClosed
Clinical recordsChart access clinic-scoped; PHI access audit logging across chart and appointment surfacesClosed
Public bookingInsecure public appointment API removed rather than patched; login redirects guardedClosed
UploadsUpload validators and log redactionClosed
Browser surfaceContent-Security-Policy enforced via django-csp; bucket origins allowed only where object storage is activeClosed
Public formsHoneypot and minimum-fill-time bot checks; real client IPs recorded behind proxy; failed logins loggedClosed

Two things I would want a reviewer to take from this. The unrouted insecure viewsets were deleted, not fixed — dead code that would have been dangerous the day someone wired it up. And the remediation includes a test asserting the public router exposes only the intended endpoints, because the failure mode was never the code, it was the routing.

Method

What makes agent-built code hold together

Every commit in this repository is mine, and most of the code was written with AI agents. At 922 commits that only works with scaffolding, and the scaffolding is the actual skill.

A conventions file agents actually obey

The repository's CLAUDE.md leads with the one rule that breaks everything when violated: all Django commands run inside Docker, never on the host. Not style preferences — the constraint that produces silent, hard-to-diagnose failure.

Purpose-built subagents

A code reviewer, a dental-practice architect, and a Django/React architect, each with its own brief. Domain review and implementation are different jobs and get different agents.

A service layer, enforced

Ten apps carry a services/ package. Business logic lives there, not on models or in views — the boundary that keeps generated code from sprawling into the wrong layer.

Tests as the acceptance gate

416 test modules plus 77 Playwright end-to-end suites, with seeded fixtures and a critical-path subset. Generated code is cheap; the suite is what makes it safe to accept.

Design docs before implementation

Specs written, reviewed against the existing models, and explicitly marked not-yet-built. The voice-charting layer has three of them and zero migrations.

An adversarial pass on my own output

The security review exists because code you did not type by hand deserves more scrutiny, not less. Thirteen findings on a codebase I wrote is the argument for the practice.

Stack

What it runs on

BackendDjango 5.2, Django REST Framework 3.15, drf-spectacular for OpenAPI
DataPostgreSQL 17, UUID primary keys, soft delete with audit mirroring
AsyncCelery + Redis — reminders, recalls, review requests, calendar sync
AuthDjango sessions, Knox tokens, passwordless patient login by magic link and SMS OTP, django-axes lockout, Google SSO for staff
FrontendStimulus.js + TypeScript progressively enhancing Django templates; Jest
IntegrationsGoogle Calendar, Twilio SMS, WhatsApp Business API and a Baileys WhatsApp-Web bridge running as its own Node service
OpsDocker Compose, nginx, nightly media-volume backups with rotation, PgHero, Sentry with a PHI scrubber
i18nEnglish, Spanish, Brazilian Portuguese

In short

What this demonstrates

  • Django at production depth — not a CRUD demo: multi-tenancy, a clinical record with lock semantics, seven-way scheduling conflict detection, PHI handling and audit logging.
  • Domain modelling as the real work — FDI notation, per-surface validation, six-site periodontal measurements, CDT codes. The schema had to be right before anything else could be.
  • AI-assisted delivery with the guardrails that make it credible — a conventions file, purpose-built subagents, an enforced service layer, 493 test modules and suites, and specs written before code.
  • The judgment to leave things unbuilt — and to audit, and delete, my own work.