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
Panel de OMSTA con reservas del mes, cobrado, comisión, saldo por cobrar y alertas operativas, sobre datos sintéticos
El mes de la agencia de un vistazo, con alertas que llevan a la acción.
Ficha de una reserva de hotel con 70 % cobrado, escalera de estados y saldo pendiente
Una reserva dice cuánto se vendió, cuánto se cobró y cuánto falta.
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.
Pestaña de pagos de una reserva con la agenda de pago al suplidor programada antes de la entrada
La fecha de pago al hotel se calcula sola desde la entrada.
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.
Formulario de reserva de vuelo con navegación rápida por secciones y resumen de precio en vivo
Alta guiada por secciones, con el precio resumido en vivo.
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.
Inicio de la app móvil con pagos vencidos, próximos check-in, saldo por cobrar, cobrado y comisión del mes
El mes de la agencia, en el bolsillo.
Formulario de la app para registrar un cobro que queda por verificar hasta que contabilidad lo aplica
Cobrar fuera de la oficina; contabilidad lo verifica después.
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.
Selector de seis tipos de producto y un borrador guardado para continuar
Seis productos y borradores que no se pierden.
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ó.
Pago contabilizado que no se edita directamente y ofrece comprobantes, corrección guiada o reembolso
Un pago contabilizado no se edita: se corrige o se reembolsa.
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.
Detalle de un asiento que explica en lenguaje llano su débito, su crédito y su origen
El asiento explica en español qué registra y por qué.
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.
Salud contable de solo lectura, sin bloqueos de cierre y con nueve de nueve identidades cuadradas
Un auditor interno: identidades contables que deben cuadrar.
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.

Inspector20 decisiones

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.

Arquitectura22 módulos · 31 conexiones

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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
Portada de Izak's Photos con un retrato en luz cálida y botones para ver la galería y reservar
La foto manda y las dos acciones principales se ven desde el primer vistazo.
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.
Foto de boda del mosaico destacado con la etiqueta First Dance al pasar el ratón
Cada foto destacada muestra su serie y abre esa imagen en el visor.
Mosaico de fotos que mezcla retrato, boda, editorial y viaje en cuatro columnas
«Todo» intercala las series para que el mosaico no se agrupe por tipo.
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.
Serie de viajes con fotos verticales y horizontales de estrellas, calles, flores y paisajes
Verticales y horizontales conviven porque cada foto reserva su proporción.
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.
Visor a pantalla completa con la foto Red Motion, su título y el contador 02 de 42
Cada foto tiene un enlace propio que abre directamente el visor.
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.
Formulario con el campo de correo marcado en rojo y el mensaje Enter a valid email address
Si la API rechaza un dato, se marca el campo exacto y se explica.
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.
Portada en español con el titular Retratos en luz tranquila y el selector de idioma en ES
Todo el sitio existe en inglés y en español, y recuerda la elección.
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.
Mosaico de dos columnas en móvil con Stillness, Newsprint, Red Motion y Vows rotuladas
En táctil cada foto lleva su lugar y su título a la vista.
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.
Confirmación de la reserva en móvil con el resumen de la sesión y el mensaje de guardado
Tras enviar, la página baja sola hasta la confirmación.
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.

Inspector17 decisiones

Arquitectura17 módulos · 19 conexiones

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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 Entropy con índice lateral, infobox de termodinámica y un enlace rojo
Cada artículo combina índice, infobox, citas y enlaces rojos a lo que falta.
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.
Artículo Black hole en tema oscuro con la imagen de M87 y su crédito de licencia
Tema oscuro completo; cada imagen de Wikimedia muestra su autor y su licencia.
Tarjeta de vista previa del artículo Light sobre el artículo Black hole
Al pasar sobre un enlace aparece un resumen, sin salir de la lectura.
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.
Buscador de Wikiverse con sugerencias para mer, Mercury planeta, elemento y desambiguación
El buscador sugiere títulos mientras se escribe y tolera erratas.
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.
Resultados de búsqueda para energy con fragmentos resaltados, filtro de categoría y orden
Búsqueda de texto completo en PostgreSQL, con ranking y coincidencias resaltadas.
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.
Comparación lado a lado de dos revisiones de Marie Curie con la sección Legacy añadida
Cualquier par de revisiones se compara lado a lado, por líneas y palabras.
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.
Editor del artículo Entropy con los campos a la izquierda y la vista previa a la derecha
El editor previsualiza con el mismo render que el artículo publicado.
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.
Artículo Coral reef en un teléfono con la imagen principal y la infobox
Los artículos se leen cómodos en el teléfono, sin columnas laterales.
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.
Comparación de revisiones de Coral reef adaptada a la pantalla de un teléfono
La comparación de versiones pasa a una columna en el teléfono.
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.

Inspector18 decisiones

Arquitectura18 módulos · 17 conexiones

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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
Feed de Network con el editor, las pestañas For you y Following, publicaciones, sugerencias de a quién seguir y hashtags en tendencia
Publicar, conversar y descubrir personas en una sola vista de tres columnas.
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.
Detalle de una publicación con su imagen, sus acciones y el hilo de comentarios
El detalle reúne la publicación y toda su conversación.
Hilo de comentarios con uno resaltado por un anillo al llegar desde una notificación
El aviso lleva al comentario exacto y lo resalta.
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.
Feed con tres tarjetas esqueleto mientras cargan las publicaciones
Mientras carga, el esqueleto ocupa el sitio exacto de la tarjeta.
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.
Centro de notificaciones con menciones, citas, likes, comentarios, reposts y respuestas, cada una con su icono y su color
Cada tipo de aviso con su icono, su color y su frase.
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.
Diálogo para borrar la cuenta que enumera lo que se elimina y pide la contraseña
Borrar la cuenta enumera lo que se pierde y pide la contraseña.
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.
Feed Following vacío en una cuenta nueva, con una explicación y un enlace para encontrar personas
El vacío explica qué irá ahí y enlaza a la acción que lo llena.
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.
Ajustes en modo oscuro con la opción Dark seleccionada
Claro, oscuro o el del sistema, aplicado antes de pintar.
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.
Feed en el teléfono con el editor, las pestañas y la barra de navegación inferior
Una columna y una barra inferior con cuatro destinos.
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.

Inspector27 decisiones

Arquitectura27 módulos · 36 conexiones

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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
Portada de Delicaté con el titular «Cuidado hecho con amor» y jabones artesanales con coco, naranja y avena
La portada vende la marca antes que el producto, con una sola acción principal.
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.
Sección Nuestra historia con una foto de jabones cortándose a mano y el texto de la marca
La historia de la marca respalda el precio de lo artesanal.
Ficha de Cacao & Almendras con precio, existencias, beneficio, tipo de piel e ingredientes
La ficha responde beneficio, piel, ingredientes y existencias sin salir del catálogo.
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.
Catálogo filtrado Para regalar con Corazón de Lavanda y Flor de Ámbar marcado como agotado
Un producto sin existencias sigue a la vista, pero no se puede agregar.
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.
Carrito con Avena Calma, Corazón de Lavanda y Café Despierto y un total estimado de RD$1,575
El carrito arma el pedido y aclara que el sitio no cobra.
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.
Carrito con el aviso de actualización de precios y disponibilidad y Avena Calma limitada a 12 unidades
Al volver, el carrito se actualiza con los precios y las existencias de hoy.
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ó.
Mensaje de catálogo no disponible con el botón Volver a intentar
Si la API falla, se ofrece reintentar, nunca un catálogo inventado.
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.
Administración de Django con diez productos y precio, existencias, destacado y activo editables
Precio, existencias y visibilidad se cambian desde la propia lista.
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.
Carrito en un teléfono con tres productos y el botón verde Finalizar por WhatsApp
El pedido sale por WhatsApp desde el mismo teléfono.
Tarjeta de Arcilla & Avena en móvil con el filtro Botánicos activo y el botón Agregar visible
En táctil, «Agregar» siempre está visible y los filtros se deslizan.
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.

Inspector18 decisiones

Arquitectura18 módulos · 19 conexiones

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

  1. 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.
  2. 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.
  3. 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.
  4. 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ó.
  5. 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.
  6. 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.
  7. 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.
  8. 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