Product ownership
En producciónQué supone de verdad ser dueño de un SaaS en producción de extremo a extremo.
Además de apsis, construyo y opero un SaaS multi-tenant de gestión de consulta clínica: agenda, ficha de paciente, notas de sesión, facturación y cumplimiento RGPD para consultas de salud en España. Soy dueño de todas sus capas: el modelo de dominio, la API y el frontend, la infraestructura donde corre, el proceso de release y las decisiones de producto que hay detrás. Esta página no nombra el producto porque opera en un sector regulado con datos personales sensibles; todo lo demás es real y está contrastado con el código.
- 131
- endpoints
- 38
- tablas
- 43
- migraciones
- 1.522
- tests
- 85%
- cobertura mínima
- 5
- releases
El tenancy como invariante de la base de datos
Cada tabla de tenant lleva su organization id, y no se confía en que ninguna query lo recuerde: una base de repositorio tenant-scoped inyecta el filtro en cada lectura, asigna el tenant en cada escritura y rechaza de plano las escrituras cruzadas. Un meta-test tira la CI si algún modelo de tenant recibe un repositorio que se salte esa base. Un nivel más abajo, las foreign keys son compuestas (id más organization id), de modo que ni una escritura con un bug puede referenciar filas de otro tenant. El aislamiento se sostiene porque el esquema lo impone, no porque cada desarrollador se acuerde.
Datos sensibles y RGPD como features
Las notas clínicas van cifradas por columna con AES-256-GCM, y cada columna deriva su propia clave vía HKDF, así que un blob descifrado en un contexto no sirve en otro. El RGPD está implementado con los artículos delante: exportación de los 15 y 20 (un ZIP con PDF y JSON) y derecho al olvido del 17 tras un gate en dos pasos, donde la purga corre en dry-run hasta que una aprobación auditada del delegado de protección de datos, con caducidad, la pasa a enforce. Cada acción queda en un audit log particionado por mes.
La base de datos como capa de concurrencia
La doble reserva es imposible a nivel de esquema: las citas son rangos TSTZRANGE bajo dos constraints EXCLUDE USING GIST, una por profesional y otra por sala, así que la carrera que los chequeos en aplicación siempre dejan abierta directamente no puede hacer commit. Los números de factura son correlativos por organización, serie y ejercicio fiscal, emitidos bajo un contador FOR UPDATE. Y todo el trabajo en segundo plano (eventos de dominio, jobs recurrentes, notificaciones) corre sobre workers nativos de Postgres: un outbox transaccional, LISTEN/NOTIFY, SKIP LOCKED y leases.
Lo que enseñó producción
El changelog guarda las cicatrices. El worker del outbox despachaba dos veces durante un rolling deploy porque los locks de fila se liberaban al hacer commit dentro del bucle: ahora reclama exactamente un evento por transacción. El planificador de jobs competía consigo mismo con dos workers hasta que los claims llevaron lease. Los huecos calculados a las "09:00" se desplazaban una hora con el cambio horario porque el wall-clock se trataba como UTC. Y la constraint EXCLUDE no veía los bloqueos sincronizados desde un calendario externo, así que un chequeo explícito de conflicto la respalda.
Disciplina de entrega
Las releases van etiquetadas con SemVer y changelog curado, y más de cincuenta architecture decision records documentan por qué las cosas son como son. La CI ejecuta seis gates en cada cambio: lint y formato, tipado estricto, contratos de import que fuerzan las capas, auditoría de CVEs en dependencias, lint del contenedor y tests a tres niveles (unitarios, funcionales contra la app ASGI e integración contra un Postgres real). La cobertura mínima exigida es del 85 por ciento, y la suite de tests es más grande que la aplicación que prueba.
Operación
Se distribuye como imagen Docker multi-stage y se despliega por SSH tras una CI verde. La observabilidad es self-hosted: Prometheus, Grafana, Loki y Tempo, con trazas OpenTelemetry a través de FastAPI, SQLAlchemy y los clientes HTTP, pruebas de carga con k6 y un filtro de PII que enmascara datos personales tanto en logs como en trazas. Las integraciones van detrás de drivers intercambiables (un fake en tests, el servicio real en producción): email transaccional, object storage con jurisdicción UE y una sincronización bidireccional de calendario con sync tokens incrementales y renovación de canal por webhook.