Endurance / Mesa de ingeniería
Proyectos
OMSTA
ERP y app móvil para una agencia de viajes
En producción diaria · app móvil en desarrollo
- Python
- Django
- Django REST Framework
- PostgreSQL
- Redis
- Django Q2
- React Native
- Expo
- TypeScript
- Railway










Cliente
Web Django
HTML renderizado en el servidor, sin SPA ni build step, para que la lógica crítica y los permisos vivan sólo en el backend.
- Tecnologías
- Django Templates · Bootstrap 5.3 · jQuery 3.7 · Select2 · HTMX 2.0 · Chart.js 4.4
- Entrega a
- Railway web
HTML renderizado en el servidor, sin SPA ni build step, para que la lógica crítica y los permisos vivan sólo en el backend.
Django Templates · Bootstrap 5.3 · jQuery 3.7 · Select2 · HTMX 2.0 · Chart.js 4.4
Un segundo cliente que no calcula saldos ni estados: pinta lo que describe el servidor.
React Native 0.86 · Expo SDK 57 · Expo Router · TypeScript 6.0 · TanStack Query 5 · react-native-mmkv · Expo SecureStore
App Django propia con JWT ligado al dispositivo y un contrato OpenAPI que genera los tipos TypeScript de la app.
Django REST Framework 3.16 · Simple JWT 5.5 · drf-spectacular 0.30 · openapi-typescript 7.13
Acceso cerrado por defecto: login obligatorio, roles efectivos (rol más módulos) y reglas por prefijo de URL en un middleware.
Django 5.2 · django-user-agents · django-crum
Ocho tipos de producto en un dominio que habla con contabilidad sólo por un contrato neutral, protegido por tests de frontera.
Django 5.2 · django-filter · django-select2 · django-countries
Un único servicio de registro para web y móvil; todo pago nace «por verificar», con clave de idempotencia, y contabilidad lo aplica.
Django 5.2 · Django REST Framework · django-storages
La factura publica un paquete de asientos atómico para que la CxC nunca quede acreedora ni los anticipos sin reclasificar.
Django 5.2 · openpyxl 3.1
Capa contable genérica que no importa contabilidad y bloquea la publicación en períodos cerrados.
Django 5.2 · PostgreSQL
ReportLab 4.4 · XlsxWriter 3.2 · Chart.js 4.4
Snapshot fiscal inmutable al contabilizar; genera el TXT oficial y el XLSX, y la presentación a la DGII queda en manos humanas.
Django 5.2 · openpyxl 3.1
Una cuenta bancaria por moneda y la tasa de cambio congelada en cada pago en pesos o dólares.
Chart.js 4.4 · chartjs-plugin-zoom · openpyxl 3.1
Recibos renderizados en vivo; sólo la factura con NCF es inmutable.
WeasyPrint 65 · ReportLab 4.4 · qrcode 8.0 · bleach 6.3
Única fuente de verdad: todo lo contable, auditable o durable vive aquí.
PostgreSQL · psycopg 3.3
Sólo cola de Django Q2, caché y sesiones; puede perderse sin perder estado de negocio.
Redis · redis-py 6.1 · hiredis
Comprobantes y documentos en almacenamiento privado con URL firmada, porque contienen datos personales y financieros.
django-storages 1.14 · boto3 1.42
Cada push a la rama principal despliega producción y demo tras lint, checks y pytest en GitHub Actions.
GitHub Actions · pytest 8.4 · Ruff · Black · Jest
Gunicorn con hilos, reciclado escalonado de procesos y una sonda /health/ exenta de login y de la redirección HTTPS.
Railway · Railpack · Gunicorn 23 · WhiteNoise 6.12
Builds de desarrollo internas probadas en un teléfono Android real; las tiendas esperan a las cuentas del negocio.
EAS Build · Expo Dev Client · expo-doctor
Django Q2 en lugar de Celery; los trabajos se encolan al confirmar la transacción y son idempotentes.
Django Q2 1.9 · Redis
Django email · SMTP
Consulta por IP cacheada 24 horas (la negativa, una) para no pagar ni esperar la geolocalización en cada petición.
IPinfo API · requests
Se consulta desde el servidor, para ubicar hoteles sin exponer la llave en la app.
Google Places API · Google Maps JavaScript API
Alcance
- apps Django en un monolito modular
- 19
- modelos de dominio
- 150
- pantallas en la app móvil
- 54
Decisiones de diseño
- Problema: Una reserva mezcla estado de venta, cobros y saldo, y hay que leerla de un vistazo. Decisión: Escalera de estados, porcentaje cobrado y saldo en la cabecera, con venta y cobro separados.
- Problema: Fuera de la oficina, un vendedor necesita cobrar sin abrir la web y sin saltarse a contabilidad. Decisión: El cobro desde el teléfono usa el mismo servicio que la web y queda «por verificar» hasta que contabilidad lo aplica.
- Problema: Editar un pago ya contabilizado descuadraría banco, asientos y saldos. Decisión: El pago contabilizado no se edita; se gestionan comprobantes, se corrige de forma guiada o se reembolsa.
- Problema: Para quien vende, la contabilidad es una caja negra. Decisión: Cada asiento explica en lenguaje llano qué registra, y la reserva enseña todos sus asientos.
- Problema: Pagar tarde al hotel pone en riesgo la reserva del cliente. Decisión: La fecha de pago al proveedor se calcula sola desde la entrada y se agenda por vencimiento.
- Problema: Un error contable descubierto al cerrar el mes llega tarde y sale caro. Decisión: Salud contable de solo lectura y un cierre que se bloquea si alguna identidad no cuadra.
- Problema: Seis tipos de producto con datos distintos acaban en seis formularios que no se parecen. Decisión: Un mismo patrón de alta para todos, con navegación por secciones y el precio resumido en vivo.
- Problema: Dar de alta una reserva en el teléfono es largo, y una llamada puede interrumpirla a medias. Decisión: Un asistente por pasos que guarda cada borrador cifrado en el teléfono; la reserva se retoma donde se dejó.
Sistema
- Web Django · Cliente
- HTML renderizado en el servidor, sin SPA ni build step, para que la lógica crítica y los permisos vivan sólo en el backend.
- App móvil · Cliente
- Un segundo cliente que no calcula saldos ni estados: pinta lo que describe el servidor.
- API móvil · Servicio
- App Django propia con JWT ligado al dispositivo y un contrato OpenAPI que genera los tipos TypeScript de la app.
- Seguridad · Servicio
- Acceso cerrado por defecto: login obligatorio, roles efectivos (rol más módulos) y reglas por prefijo de URL en un middleware.
- Reservas y CRM · Servicio
- Ocho tipos de producto en un dominio que habla con contabilidad sólo por un contrato neutral, protegido por tests de frontera.
- Cobros · Servicio
- Un único servicio de registro para web y móvil; todo pago nace «por verificar», con clave de idempotencia, y contabilidad lo aplica.
- Contabilidad · Servicio
- La factura publica un paquete de asientos atómico para que la CxC nunca quede acreedora ni los anticipos sin reclasificar.
- Libro mayor · Servicio
- Capa contable genérica que no importa contabilidad y bloquea la publicación en períodos cerrados.
- Nómina · Servicio
- Fiscal DGII · Servicio
- Snapshot fiscal inmutable al contabilizar; genera el TXT oficial y el XLSX, y la presentación a la DGII queda en manos humanas.
- Banco y divisas · Servicio
- Una cuenta bancaria por moneda y la tasa de cambio congelada en cada pago en pesos o dólares.
- Documentos · Servicio
- Recibos renderizados en vivo; sólo la factura con NCF es inmutable.
- PostgreSQL · Datos
- Única fuente de verdad: todo lo contable, auditable o durable vive aquí.
- Redis · Datos
- Sólo cola de Django Q2, caché y sesiones; puede perderse sin perder estado de negocio.
- Bucket S3 · Datos
- Comprobantes y documentos en almacenamiento privado con URL firmada, porque contienen datos personales y financieros.
- GitHub CI · Infraestructura
- Cada push a la rama principal despliega producción y demo tras lint, checks y pytest en GitHub Actions.
- Railway web · Infraestructura
- Gunicorn con hilos, reciclado escalonado de procesos y una sonda /health/ exenta de login y de la redirección HTTPS.
- EAS Build · Infraestructura
- Builds de desarrollo internas probadas en un teléfono Android real; las tiendas esperan a las cuentas del negocio.
- Worker Q2 · Infraestructura
- Django Q2 en lugar de Celery; los trabajos se encolan al confirmar la transacción y son idempotentes.
- Correo SMTP · Integraciones
- IPinfo · Integraciones
- Consulta por IP cacheada 24 horas (la negativa, una) para no pagar ni esperar la geolocalización en cada petición.
- Google Places · Integraciones
- Se consulta desde el servidor, para ubicar hoteles sin exponer la llave en la app.
Izak's Photos
Sitio y portafolio fotográfico
En línea en Railway · estudio de demostración
- React 18
- Vite 8
- React Router 7
- Django 5.2
- Django REST Framework
- Railway









Hero a sangre con botón de pausa y «Reserve» fijo en la cabecera; las fotos destacadas abren esa imagen en el visor.
React 18 · React Router 7
El mosaico intercala las series y cada foto reserva su proporción; la categoría vive en la URL.
useSearchParams · CSS propio
Teclado, deslizamiento y foco dentro del diálogo; la foto abierta es un enlace y su miniatura la cubre mientras llega el JPEG.
React 18 · useSearchParams
En táctil, el lugar y el título de cada foto siempre visibles: no hay hover que los descubra.
@media (hover: none) · CSS propio
Las fotos se rotulan como obra del portafolio y no como retrato de nadie.
React 18
Errores por campo que devuelve la API y, tras enviar, un resumen al que la página baja en el teléfono.
fetch · aria-invalid
Un Context propio con t({ en, es }), sin librería de traducción; el idioma se recuerda y cambia el lang de la página.
React Context · localStorage
Valida y guarda cada solicitud; diez por hora y cliente, y un campo trampa que acepta a los bots sin guardarlos.
Django REST Framework 3 · Throttling de DRF
Filtros, búsqueda y dos acciones: marcar solicitudes como atendidas o reabrirlas.
Django Admin
Railway sólo publica la versión nueva si /api/health/ responde.
Django 5.2
SQLite en local y PostgreSQL con DATABASE_URL, sin tocar código; un solo modelo de diez campos para las solicitudes.
SQLite · PostgreSQL · dj-database-url
Van en el build: el JPEG completo para el visor y el hero, y miniaturas WebP de 720 px para las cuadrículas.
import.meta.glob · WebP
Sólo guarda el idioma elegido: el sitio no tiene cuentas ni cookies de seguimiento.
localStorage
Railpack compila todo, migra antes de publicar y reinicia el servicio si el healthcheck falla.
Railway · Railpack
Un solo proceso sirve el build de Vite, la API y el admin desde el mismo origen: sin CORS en producción.
Gunicorn 23 · WhiteNoise 6.12
En cada push y pull request: migraciones pendientes, las 8 pruebas y el build del frontend.
GitHub Actions
Fraunces y DM Sans: la única dependencia externa en tiempo de ejecución.
Google Fonts
Alcance
- de peso en las miniaturas de la galería
- −74 %
- fotos en 4 series, cada una con su enlace
- 42
- idiomas, inglés y español
- 2
Decisiones de diseño
- Problema: El sitio tiene que lucir el trabajo y, a la vez, llevar a reservar. Decisión: Hero a sangre con dos acciones y «Reserve» fijo en la cabecera de todas las páginas.
- Problema: Cuarenta y dos fotos en cuatro series se vuelven una cuadrícula genérica. Decisión: Un mosaico que intercala las series, filtros con conteo y un visor a pantalla completa con teclado.
- Problema: No había forma de compartir ni de enlazar una foto concreta. Decisión: La categoría y la foto abierta viven en la URL; cada foto tiene su enlace.
- Problema: En el teléfono los títulos no se veían nunca, porque sólo aparecían al pasar el ratón. Decisión: En pantallas táctiles, el lugar y el título quedan siempre visibles sobre cada foto.
- Problema: La galería descargaba 9,3 MB de JPEG completos para mostrar miniaturas pequeñas. Decisión: Miniaturas WebP de 720 px en las cuadrículas; el JPEG completo sólo se carga en el visor.
- Problema: En el teléfono, la confirmación de la reserva quedaba fuera de la pantalla. Decisión: Tras enviar, la página baja hasta la confirmación, que resume lo enviado.
- Problema: Un error genérico obliga a adivinar qué dato falló y a volver a escribirlo todo. Decisión: La API devuelve el error de cada campo y el formulario lo marca en su sitio, sin borrar lo escrito.
- Problema: Un estudio que trabaja dentro y fuera del país no puede hablar un solo idioma. Decisión: Inglés y español en todo el sitio con un clic; el idioma se recuerda y cambia el lang de la página.
Sistema
- Portada · Cliente
- Hero a sangre con botón de pausa y «Reserve» fijo en la cabecera; las fotos destacadas abren esa imagen en el visor.
- Galería · Cliente
- El mosaico intercala las series y cada foto reserva su proporción; la categoría vive en la URL.
- Visor · Cliente
- Teclado, deslizamiento y foco dentro del diálogo; la foto abierta es un enlace y su miniatura la cubre mientras llega el JPEG.
- En el teléfono · Cliente
- En táctil, el lugar y el título de cada foto siempre visibles: no hay hover que los descubra.
- Sobre mí · Cliente
- Las fotos se rotulan como obra del portafolio y no como retrato de nadie.
- Reservas · Cliente
- Errores por campo que devuelve la API y, tras enviar, un resumen al que la página baja en el teléfono.
- Idioma · Cliente
- Un Context propio con t({ en, es }), sin librería de traducción; el idioma se recuerda y cambia el lang de la página.
- API de reservas · Servicio
- Valida y guarda cada solicitud; diez por hora y cliente, y un campo trampa que acepta a los bots sin guardarlos.
- Admin · Servicio
- Filtros, búsqueda y dos acciones: marcar solicitudes como atendidas o reabrirlas.
- Healthcheck · Servicio
- Railway sólo publica la versión nueva si /api/health/ responde.
- Base de datos · Datos
- SQLite en local y PostgreSQL con DATABASE_URL, sin tocar código; un solo modelo de diez campos para las solicitudes.
- Fotos · Datos
- Van en el build: el JPEG completo para el visor y el hero, y miniaturas WebP de 720 px para las cuadrículas.
- localStorage · Datos
- Sólo guarda el idioma elegido: el sitio no tiene cuentas ni cookies de seguimiento.
- Railway · Infraestructura
- Railpack compila todo, migra antes de publicar y reinicia el servicio si el healthcheck falla.
- Servidor · Infraestructura
- Un solo proceso sirve el build de Vite, la API y el admin desde el mismo origen: sin CORS en producción.
- GitHub CI · Infraestructura
- En cada push y pull request: migraciones pendientes, las 8 pruebas y el build del frontend.
- Google Fonts · Integraciones
- Fraunces y DM Sans: la única dependencia externa en tiempo de ejecución.
Wikiverse
Enciclopedia web full-stack
En producción en Railway · dominio propio
- React 19
- TypeScript
- Django REST Framework
- PostgreSQL
- Redis
- Railway









Artículo destacado, curiosidades y efemérides que salen de bloques editables; la portada se guarda en caché por tiempo.
React 19 · TanStack Query 5
Por debajo de 960 px el índice pasa a la cabecera y la infobox se apila; ninguna pantalla se desborda a 390 px.
Tailwind CSS 4.3
El Markdown se renderiza en el cliente con un plugin propio para wikilinks, notas al pie y enlaces rojos, sin HTML crudo.
react-markdown 9 · remark-gfm 4 · Plugin remark propio
La consulta, el filtro y el orden viven en la URL: una búsqueda se puede enlazar y el botón atrás funciona.
React Router 7 · TanStack Query 5
Dos revisiones cualesquiera se comparan lado a lado o en una columna, con el delta de bytes de cada cambio.
React 19
Ediciones y mensajes de discusión en un solo feed, con cada filtro en la URL y un feed RSS.
React Router 7
Se carga sólo al usarlo, previsualiza con el mismo render que el artículo y guarda borradores sin restaurarlos solo.
React.lazy · localStorage
36 rutas documentadas con OpenAPI; JWT de 30 minutos con refresco rotado y revocado al cerrar sesión.
Django REST Framework 3.17 · SimpleJWT 5.5 · drf-spectacular 0.30
Una revisión inmutable por guardado, con padre, autor y delta; un guardado sin cambios no crea ninguna.
Django 5.2
websearch_to_tsquery con ranking, trigram cuando no hay resultados y fragmentos escapados antes de resaltar.
PostgreSQL full-text · pg_trgm
nginx envía sólo los bots de vista previa a Django, que responde con Open Graph y JSON-LD; las personas reciben la SPA.
Django 5.2 · Open Graph
Estadísticas, portada, sugerencias, diffs y límites de peticiones: perder Redis cuesta rendimiento, no datos.
Redis · django-redis 5.4
63 artículos en 21 módulos validados con 31 reglas; sembrar dos veces da exactamente la misma historia.
Comando seed propio
Un trigger mantiene el vector de búsqueda con pesos A, B y C; lo exclusivo de Postgres se decide al llamar, no al importar.
PostgreSQL 18 en Railway · tsvector + GIN
Frontend, backend, PostgreSQL y Redis como cuatro servicios; se despliega al hacer push a main.
Railway · Docker
Sirve la SPA, hace de proxy de /api y emite la misma CSP que Django; index.html sin caché y los assets con hash para siempre.
nginx 1.27
608 pruebas y además migraciones completas, esquema OpenAPI sin avisos y el corpus validado antes de fusionar.
GitHub Actions · pytest · Vitest · Playwright
Imágenes sólo de Wikimedia Commons, con autor y licencia; la lista blanca se valida también en el servidor.
Wikimedia Commons
Alcance
- pruebas en CI, del backend a Playwright
- 608
- artículos escritos, con 428 referencias
- 63
- rutas de API documentadas con OpenAPI
- 36
Decisiones de diseño
- Problema: Un texto que cualquiera edita pierde confianza si no se ve quién cambió qué. Decisión: Cada guardado es una revisión inmutable con autor, resumen y delta; dos cualesquiera se comparan palabra a palabra.
- Problema: Comparar palabra por palabra un artículo largo de una sola pasada tardaba decenas de segundos. Decisión: El diff compara primero líneas y sólo después palabras dentro de las líneas cambiadas.
- Problema: Un índice de búsqueda que el código tiene que acordarse de actualizar acaba desfasado. Decisión: Un trigger de PostgreSQL recalcula el vector ponderado de título, resumen y cuerpo en cada cambio.
- Problema: Una búsqueda con una errata no encuentra nada y el lector se va. Decisión: Sugerencias mientras se escribe y similitud trigram como segundo intento; «entrpy» encuentra Entropy.
- Problema: Abrir cada enlace para saber de qué trata interrumpe la lectura. Decisión: Al pasar el ratón, una tarjeta muestra la descripción corta y el extracto del artículo enlazado.
- Problema: Un enlace a un artículo que aún no existe suele romperse o esconderse. Decisión: Se muestra en rojo, abre el editor con el título listo y se vuelve azul solo cuando alguien lo escribe.
- Problema: Una vista previa con otro render acaba mostrando algo distinto de lo que se publica. Decisión: El editor previsualiza con el mismo pipeline de Markdown que el artículo.
- Problema: Índice, artículo y herramientas en tres columnas no caben en un teléfono. Decisión: Por debajo de 960 px el índice pasa a la cabecera y la infobox se apila; nada se desborda a 390 px.
Sistema
- Portada · Cliente
- Artículo destacado, curiosidades y efemérides que salen de bloques editables; la portada se guarda en caché por tiempo.
- En el teléfono · Cliente
- Por debajo de 960 px el índice pasa a la cabecera y la infobox se apila; ninguna pantalla se desborda a 390 px.
- Artículo · Cliente
- El Markdown se renderiza en el cliente con un plugin propio para wikilinks, notas al pie y enlaces rojos, sin HTML crudo.
- Búsqueda · Cliente
- La consulta, el filtro y el orden viven en la URL: una búsqueda se puede enlazar y el botón atrás funciona.
- Historial · Cliente
- Dos revisiones cualesquiera se comparan lado a lado o en una columna, con el delta de bytes de cada cambio.
- Cambios · Cliente
- Ediciones y mensajes de discusión en un solo feed, con cada filtro en la URL y un feed RSS.
- Editor · Cliente
- Se carga sólo al usarlo, previsualiza con el mismo render que el artículo y guarda borradores sin restaurarlos solo.
- API REST · Servicio
- 36 rutas documentadas con OpenAPI; JWT de 30 minutos con refresco rotado y revocado al cerrar sesión.
- Revisiones · Servicio
- Una revisión inmutable por guardado, con padre, autor y delta; un guardado sin cambios no crea ninguna.
- Buscador · Servicio
- websearch_to_tsquery con ranking, trigram cuando no hay resultados y fragmentos escapados antes de resaltar.
- Vista para bots · Servicio
- nginx envía sólo los bots de vista previa a Django, que responde con Open Graph y JSON-LD; las personas reciben la SPA.
- Redis · Datos
- Estadísticas, portada, sugerencias, diffs y límites de peticiones: perder Redis cuesta rendimiento, no datos.
- Corpus · Datos
- 63 artículos en 21 módulos validados con 31 reglas; sembrar dos veces da exactamente la misma historia.
- PostgreSQL · Datos
- Un trigger mantiene el vector de búsqueda con pesos A, B y C; lo exclusivo de Postgres se decide al llamar, no al importar.
- Railway · Infraestructura
- Frontend, backend, PostgreSQL y Redis como cuatro servicios; se despliega al hacer push a main.
- nginx · Infraestructura
- Sirve la SPA, hace de proxy de /api y emite la misma CSP que Django; index.html sin caché y los assets con hash para siempre.
- GitHub CI · Infraestructura
- 608 pruebas y además migraciones completas, esquema OpenAPI sin avisos y el corpus validado antes de fusionar.
- Commons · Integraciones
- Imágenes sólo de Wikimedia Commons, con autor y licencia; la lista blanca se valida también en el servidor.
Network 3.0
Red social full-stack
Desplegado en Railway · demo pública
- React
- TypeScript
- Vite
- Tailwind CSS
- TanStack Query
- Django
- Django REST Framework
- PostgreSQL
- Redis
- Docker









Tres columnas: publicar, conversar y descubrir personas sin salir del feed, que se carga solo al llegar al final.
React 19 · IntersectionObserver
Respuestas a un solo nivel: basta para conversar sin hilos que se pierdan en profundidad.
React 19 · React Router 7
Pestañas Posts, Media y Likes; seguidores y seguidos son modales con dirección propia.
React 19 · React Router 7
Cada verbo con su icono, su color y su frase, y cada fila lleva a su origen exacto.
React 19 · lucide-react
Una sola caja para personas, texto y hashtags, con atajo /; escribir con # salta a las publicaciones.
React Router 7 · Rebote de 350 ms
Registrarse deja la sesión iniciada, y el acceso ofrece cuentas demo de un clic.
Zustand 5 · React 19
Una columna y una barra inferior de cuatro destinos; marcadores y ajustes, al menú de la cuenta.
Tailwind CSS 4 · Manifiesto instalable
Un like optimista se refleja a la vez en el feed, el detalle y el perfil, escrito en un solo sitio.
TanStack Query 5 · Zustand 5
Una sola promesa de refresco compartida: diez peticiones con el token caducado disparan un refresco, no diez.
Axios 1.20
Versionada bajo /api/v1/: 28 rutas y 37 operaciones, con el esquema OpenAPI generado del propio código.
Django REST Framework 3.17 · django-filter · django-cors-headers
Refresh rotado con lista negra y 10 intentos por minuto y por IP; cambiar la contraseña cierra las demás sesiones.
SimpleJWT 5.5 · Lista negra de tokens
Cursor por fecha e id: publicar algo nuevo no repite la página siguiente; los contadores son subconsultas, no JOIN.
CursorPagination de DRF · Subquery y Exists
Repost y cita son el mismo modelo; una restricción única condicional impide repostear dos veces lo mismo.
Django 6.0 · UniqueConstraint
Toda subida se valida, se rota por EXIF, pierde sus metadatos y se reescribe como WebP.
Pillow 12.3
Seguirse a uno mismo lo prohíbe la base de datos, no sólo la vista.
Django 6.0 · CheckConstraint
App propia con ocho verbos; al deshacer un like, un follow o un repost se borra su aviso.
Django 6.0
Se extraen al guardar; la tendencia cuenta los últimos siete días y se cachea cinco minutos.
Expresiones regulares · Caché de Django
Ocho modelos con índices escritos para las consultas que existen, como el de avisos no leídos.
PostgreSQL 17 · psycopg 3.3 · dj-database-url
Sólo tendencias y sugerencias: si cae, la aplicación se ralentiza, no se rompe.
Redis 8 · django-redis 7.0
Disco local por defecto; un bucket S3 compatible con sólo definir las variables.
django-storages 1.14
Además de lint, tipos y pruebas, valida migraciones, check --deploy y la construcción de las dos imágenes.
GitHub Actions · Ruff · ESLint · Vitest
Dos imágenes multi-stage que respetan $PORT: la misma corre en local y en Railway.
Docker · Docker Compose
Los dos servicios, configurados como código; el health-check sólo falla si cae la base de datos.
Railway · railway.json
Sirve la aplicación, aplica la CSP y las cabeceras de seguridad y devuelve index.html a las rutas del cliente.
nginx · Content-Security-Policy
WSGI: no hay nada en tiempo real que justifique ASGI.
Gunicorn 26 · WhiteNoise 6.12
El esquema sale del código: la documentación no puede quedarse vieja.
drf-spectacular 0.29 · Swagger UI · ReDoc
Integrado pero apagado salvo que exista SENTRY_DSN; no obliga a depender de un servicio externo.
Sentry SDK 2.70
Alcance
- pruebas automatizadas, backend y frontend
- 132
- operaciones de API documentadas en OpenAPI
- 37
- tipos de aviso, cada uno con su destino
- 8
Decisiones de diseño
- Problema: Lo que ves en una red social depende de a quién sigues, y a quién seguir no se descubre solo. Decisión: Publicar, conversar y descubrir personas conviven en una vista de tres columnas, sin salir del feed.
- Problema: En el teléfono no caben tres columnas ni seis destinos de navegación. Decisión: Una sola columna y una barra inferior de cuatro destinos; marcadores y ajustes pasan al menú de la cuenta.
- Problema: Un spinner centrado no dice qué viene, y al llegar el contenido la página salta. Decisión: Esqueletos con la geometría exacta de la tarjeta real; el hueco ya tiene su tamaño y nada se mueve.
- Problema: Una pantalla que sólo dice «no hay nada» parece una función a medio terminar. Decisión: Cada vacío explica qué irá ahí y ofrece la acción que lo llena, como buscar a quién seguir.
- Problema: Ocho tipos de actividad en una sola lista se leen como un muro indiferenciado. Decisión: Cada tipo lleva su icono, su color y su frase; las no leídas se marcan y cada fila lleva a su origen.
- Problema: Un aviso de comentario que sólo abre la publicación obliga a buscar el comentario en el hilo. Decisión: El enlace lleva al comentario exacto; la página baja hasta él y lo resalta con un anillo.
- Problema: «¿Estás seguro?» no dice nada; se confirma sin saber qué se pierde. Decisión: El diálogo nombra lo que se borra y, cuando no hay vuelta atrás, pide la contraseña.
- Problema: Un modo oscuro que destella en blanco al cargar, o que ignora el del sistema, se siente roto. Decisión: Claro, oscuro o el del sistema; el tema se aplica antes de que React monte, sin destello.
Sistema
- Feed · Cliente
- Tres columnas: publicar, conversar y descubrir personas sin salir del feed, que se carga solo al llegar al final.
- Publicación · Cliente
- Respuestas a un solo nivel: basta para conversar sin hilos que se pierdan en profundidad.
- Perfiles · Cliente
- Pestañas Posts, Media y Likes; seguidores y seguidos son modales con dirección propia.
- Centro de avisos · Cliente
- Cada verbo con su icono, su color y su frase, y cada fila lleva a su origen exacto.
- Búsqueda · Cliente
- Una sola caja para personas, texto y hashtags, con atajo /; escribir con # salta a las publicaciones.
- Cuenta · Cliente
- Registrarse deja la sesión iniciada, y el acceso ofrece cuentas demo de un clic.
- En el teléfono · Cliente
- Una columna y una barra inferior de cuatro destinos; marcadores y ajustes, al menú de la cuenta.
- Estado y caché · Cliente
- Un like optimista se refleja a la vez en el feed, el detalle y el perfil, escrito en un solo sitio.
- Cliente HTTP · Cliente
- Una sola promesa de refresco compartida: diez peticiones con el token caducado disparan un refresco, no diez.
- API REST v1 · Servicio
- Versionada bajo /api/v1/: 28 rutas y 37 operaciones, con el esquema OpenAPI generado del propio código.
- Auth JWT · Servicio
- Refresh rotado con lista negra y 10 intentos por minuto y por IP; cambiar la contraseña cierra las demás sesiones.
- Feed por cursor · Servicio
- Cursor por fecha e id: publicar algo nuevo no repite la página siguiente; los contadores son subconsultas, no JOIN.
- Contenido · Servicio
- Repost y cita son el mismo modelo; una restricción única condicional impide repostear dos veces lo mismo.
- Imágenes · Servicio
- Toda subida se valida, se rota por EXIF, pierde sus metadatos y se reescribe como WebP.
- Grafo social · Servicio
- Seguirse a uno mismo lo prohíbe la base de datos, no sólo la vista.
- Avisos · Servicio
- App propia con ocho verbos; al deshacer un like, un follow o un repost se borra su aviso.
- Hashtags · Servicio
- Se extraen al guardar; la tendencia cuenta los últimos siete días y se cachea cinco minutos.
- PostgreSQL · Datos
- Ocho modelos con índices escritos para las consultas que existen, como el de avisos no leídos.
- Redis · Datos
- Sólo tendencias y sugerencias: si cae, la aplicación se ralentiza, no se rompe.
- Archivos · Datos
- Disco local por defecto; un bucket S3 compatible con sólo definir las variables.
- GitHub CI · Infraestructura
- Además de lint, tipos y pruebas, valida migraciones, check --deploy y la construcción de las dos imágenes.
- Docker · Infraestructura
- Dos imágenes multi-stage que respetan $PORT: la misma corre en local y en Railway.
- Railway · Infraestructura
- Los dos servicios, configurados como código; el health-check sólo falla si cae la base de datos.
- nginx · Infraestructura
- Sirve la aplicación, aplica la CSP y las cabeceras de seguridad y devuelve index.html a las rutas del cliente.
- Gunicorn · Infraestructura
- WSGI: no hay nada en tiempo real que justifique ASGI.
- OpenAPI · Integraciones
- El esquema sale del código: la documentación no puede quedarse vieja.
- Sentry · Integraciones
- Integrado pero apagado salvo que exista SENTRY_DSN; no obliga a depender de un servicio externo.
Delicaté 4.0
E-commerce y sitio de marca
En línea con dominio propio · salida comercial en validación
- React 19
- Vite 8
- Django 5.2
- Django REST Framework
- PostgreSQL
- Docker
- Railway










La marca antes que la tienda: una sola acción principal lleva a la colección.
React 19 · CSS propio
Se lee entero de la API, siguiendo todas sus páginas, y se filtra en el navegador; los filtros declaran su estado con aria-pressed.
fetch con AbortController · React 19
Un diálogo nativo responde beneficio, piel, ingredientes y existencias sin salir del catálogo.
<dialog> nativo · React 19
Sin cuenta. Al volver se cruza con el catálogo: toma el precio actual, limita cada cantidad a las existencias y avisa de lo que cambió.
useReducer · Hook propio useCart
Pensada para el teléfono: en táctil «Agregar» siempre está visible y el carrito ocupa la pantalla entera.
@media (hover: none) · CSS propio
El formulario no guarda nada en el servidor: convierte la consulta en un mensaje de WhatsApp.
React 19
Pública y de sólo lectura: listado paginado con filtros y ficha por slug. El catálogo sólo se escribe desde el administrador.
Django REST Framework 3.17 · Paginación hasta 100
Precio, existencias, destacado y visibilidad se editan en la propia lista; las fotos se validan (JPG, PNG o WebP, hasta 5 MB).
Django Admin · Pillow 12.3
CSP y demás cabeceras en toda respuesta, HSTS de un año y límites de frecuencia que cuentan la IP real detrás del proxy.
Middleware propio · Throttling de DRF
El carrito vive en el navegador; el servidor nunca guarda pedidos ni datos de compradores.
localStorage
Cuatro modelos, frente a ocho en 2024: productos, mensajes, suscripciones y el usuario del equipo.
PostgreSQL · psycopg 3.3 · dj-database-url 3.1
Las fotos subidas viven en un volumen persistente; Django no arranca si su ruta es relativa.
Volumen de Railway · Caché de 7 días
En cada push: check de Django, migraciones pendientes, las 18 pruebas, lint y build.
GitHub Actions
Imagen multietapa: Node construye la tienda y Python la sirve, con DEBUG apagado por defecto.
node:24-alpine · python:3.13-slim
Migra antes de publicar y sólo da tráfico a la versión nueva si el healthcheck, que consulta la base, responde 200.
Railway · railway.json
Un solo proceso sirve tienda, API, administración y fotos desde el mismo origen: sin CORS.
Gunicorn 23 · WhiteNoise 6.12
El pedido sale como mensaje prearmado por wa.me, en el canal donde el negocio ya confirma, cobra y entrega.
Enlace wa.me
Al compartir el enlace, una imagen de 1200×630 y la URL canónica salen de la configuración del build.
Open Graph · Twitter Card
Alcance
- pruebas automatizadas, en CI en cada push
- 18
- cuentas necesarias para comprar
- 0
- de JavaScript en producción, con gzip
- 68 KB
Decisiones de diseño
- Problema: El precio de un jabón artesanal se sostiene con la marca, no con un listado. Decisión: La portada presenta la marca antes que la tienda, con una sola acción principal.
- Problema: Antes de comprar, el cliente pregunta por beneficio, tipo de piel e ingredientes. Decisión: La ficha lo responde en un diálogo nativo, sin salir del catálogo.
- Problema: Un checkout que no puede cobrar es una promesa rota. Decisión: El carrito arma el pedido, aclara que no cobra y lo cierra en WhatsApp.
- Problema: Un carrito guardado días antes puede enviar precios viejos o cantidades sin existencias. Decisión: Al volver, el carrito se actualiza con el catálogo real y avisa de lo que cambió.
- Problema: En una pantalla táctil no hay hover que descubra el botón de compra. Decisión: En táctil «Agregar» queda siempre visible y los filtros se desplazan en una fila.
- Problema: Si un jabón agotado desaparece del catálogo, quien lo buscaba no sabe si volverá. Decisión: Sigue a la vista con «Agotado» y la compra desactivada, igual en la tarjeta que en la ficha.
- Problema: Si la API falla, mostrar productos inventados engaña al comprador. Decisión: Producción muestra un error con reintento; el catálogo de demostración sólo existe en desarrollo.
- Problema: El negocio tenía que cambiar su catálogo sin tocar código ni recompilar. Decisión: Precio, existencias, destacado y visibilidad se editan en la propia lista del administrador.
Sistema
- Portada · Cliente
- La marca antes que la tienda: una sola acción principal lleva a la colección.
- Catálogo · Cliente
- Se lee entero de la API, siguiendo todas sus páginas, y se filtra en el navegador; los filtros declaran su estado con aria-pressed.
- Ficha · Cliente
- Un diálogo nativo responde beneficio, piel, ingredientes y existencias sin salir del catálogo.
- Carrito · Cliente
- Sin cuenta. Al volver se cruza con el catálogo: toma el precio actual, limita cada cantidad a las existencias y avisa de lo que cambió.
- En el teléfono · Cliente
- Pensada para el teléfono: en táctil «Agregar» siempre está visible y el carrito ocupa la pantalla entera.
- Contacto · Cliente
- El formulario no guarda nada en el servidor: convierte la consulta en un mensaje de WhatsApp.
- API de catálogo · Servicio
- Pública y de sólo lectura: listado paginado con filtros y ficha por slug. El catálogo sólo se escribe desde el administrador.
- Admin · Servicio
- Precio, existencias, destacado y visibilidad se editan en la propia lista; las fotos se validan (JPG, PNG o WebP, hasta 5 MB).
- Seguridad · Servicio
- CSP y demás cabeceras en toda respuesta, HSTS de un año y límites de frecuencia que cuentan la IP real detrás del proxy.
- localStorage · Datos
- El carrito vive en el navegador; el servidor nunca guarda pedidos ni datos de compradores.
- PostgreSQL · Datos
- Cuatro modelos, frente a ocho en 2024: productos, mensajes, suscripciones y el usuario del equipo.
- Fotos · Datos
- Las fotos subidas viven en un volumen persistente; Django no arranca si su ruta es relativa.
- GitHub CI · Infraestructura
- En cada push: check de Django, migraciones pendientes, las 18 pruebas, lint y build.
- Docker · Infraestructura
- Imagen multietapa: Node construye la tienda y Python la sirve, con DEBUG apagado por defecto.
- Railway · Infraestructura
- Migra antes de publicar y sólo da tráfico a la versión nueva si el healthcheck, que consulta la base, responde 200.
- Servidor · Infraestructura
- Un solo proceso sirve tienda, API, administración y fotos desde el mismo origen: sin CORS.
- WhatsApp · Integraciones
- El pedido sale como mensaje prearmado por wa.me, en el canal donde el negocio ya confirma, cobra y entrega.
- Vista previa · Integraciones
- Al compartir el enlace, una imagen de 1200×630 y la URL canónica salen de la configuración del build.
Cada misión cuenta qué problema resolvió, cómo se construyó y cuál es su estado real.
He diseñado y construido sistemas empresariales, aplicaciones web y sitios digitales. Aquí reúno el trabajo que mejor muestra cómo combino estrategia, desarrollo y diseño, con estados y evidencia verificables.
- Proyectos en producción
- 5
- Para clientes reales
- 2
- Casos completos documentados
- 5
