De Access a la nube: Caso de estudio y entrevista técnica detrás de FactuKora

De Access a la nube: Caso de estudio y entrevista técnica detrás de FactuKora

Ruperto Coronado
Ruperto Coronado

Migrar un sistema de escritorio maduro a la nube no es simplemente rescribir código en un framework de moda: es un dilema de ingeniería donde la regla de oro es no romper la operación diaria del negocio.

En esta entrevista técnica en profundidad, nos metemos en las entrañas de la arquitectura detrás de la migración de FactuKora, un sistema de punto de venta originalmente desarrollado en Microsoft Access + VBA y Microsoft SQL Server, llevado a una arquitectura híbrida con Django, PostgreSQL, Docker y Vue 3.

📖 Este post complementa la serie:

  1. ¿Se puede llevar un sistema de escritorio a la nube?
  2. Cómo llevé un sistema de escritorio a la nube: Mi experiencia y arquitectura real

1. El perímetro y la red: Conectando la nube al on-premise

Entrevistador: En tu diagrama mostrás que Django apunta como base secundaria directo al MSSQL local del cliente. ¿Cómo resolviste la conectividad de red y la seguridad sin abrir puertos al router ni exponer el puerto 1433 a Internet?

Ruperto: Usando Tailscale. Tanto el servidor de base de datos on-premise como el servidor de la aplicación en la nube están dentro de una red de malla privada (Mesh VPN basada en WireGuard).

De esta manera, la máquina local y el VPS se comunican como si estuvieran en la misma red de área local (LAN). No tuve que abrir puertos en el módem del cliente, no sufro los problemas de CGNAT ni expongo jamás el puerto de base de datos a Internet público. Solo los nodos autorizados tienen visibilidad.


2. Disponibilidad y resiliencia: La caja registradora manda

Entrevistador: Las conexiones de internet comerciales o residenciales sufren microcortes y latencia. Al estar Django consultando en vivo al MSSQL on-premise, ¿qué pasaba cuando el local se quedaba sin conexión?

Ruperto: La realidad es que si se caía el internet en la tienda, la aplicación web de Django dejaba de responder. Pero acá hay una decisión de diseño fundamental basada en la prioridad del negocio:

El 99% de la operación inicial corría físicamente en la tienda sobre el sistema de escritorio. Si la nube fallaba, al negocio no le importaba: la caja registradora seguía cobrando.

La prioridad siempre fue mantener los negocios físicos funcionando antes que la app web. La nube nació como un valor agregado para consulta y extensión, no como un punto único de fallo que pudiera paralizar las ventas físicas.


3. Ingeniería inversa: Domando el esquema legado en Django

Entrevistador: Los esquemas que nacen en Access y migran a SQL Server suelen tener peculiaridades: tablas sin claves foráneas formales, nombres en PascalCase o campos poco convencionales. ¿Cómo adaptaste eso en los modelos de Django sin romper la base viva de Access?

Ruperto: La propiedad managed = False en la clase Meta de los modelos fue crucial. Todo cambio estructural a la base de datos se siguió haciendo desde el sistema desktop para no romper Access.

Para modernizar el código en Python sin alterar las columnas reales de SQL Server, usé intensivamente db_column. De este modo, los modelos en Django adoptaron un estándar limpio en snake_case mientras por debajo siguen apuntando a los nombres legados:

# Ejemplo: tbaConfiguraciones.idcompratipodoc -> config.compra_documento
class Configuracion(models.Model):
    compra_documento = models.IntegerField(
        db_column='idcompratipodoc',
        verbose_name="Tipo de documento de compra"
    )

    class Meta:
        managed = False
        db_table = 'tbaConfiguraciones'

4. Persistencia híbrida: Cruzando PostgreSQL y SQL Server sin FKs nativas

Entrevistador: Tenías PostgreSQL para usuarios, sesiones y módulos nuevos, y MSSQL para el negocio legacy. Pero el ORM de Django no soporta joins ni Foreign Keys nativas entre distintas bases de datos. ¿Cómo resolviste las relaciones entre ambos mundos?

Ruperto: Usando relaciones simuladas y campos planos. A nivel de base de datos no existen las restricciones de clave foránea entre motores, pero en Django podés definir la relación lógica usando db_constraint=False y on_delete=models.DO_NOTHING.

Un ejemplo real fue asociar imágenes de comprobantes o evidencias a compras existentes en el motor legacy:

class CompraImagen(models.Model):
    """
    Almacena múltiples imágenes asociadas a una compra.
    Reside en PostgreSQL (postgres_db) como extensión del sistema legacy.
    """
    using_db = 'postgres_db'

    compra = models.ForeignKey(
        'CompraV2', 
        on_delete=models.DO_NOTHING, 
        related_name='imagenes_set', 
        db_column='IDCompra', 
        db_constraint=False
    )
    imagen = models.ImageField(upload_to='compras/evidencias/', verbose_name="Imagen/Evidencia")
    thumbnail = models.ImageField(upload_to='compras/thumbnails/', editable=False, null=True, blank=True)
    descripcion = models.CharField(max_length=255, blank=True, null=True, verbose_name="Descripción")
    fecha_creacion = models.DateTimeField(auto_now_add=True)

A los más puristas les puede hacer ruido a nivel teórico o de rendimiento, pero en la práctica para un sistema con un pico máximo de ~100 usuarios concurrentes, el impacto es imperceptible y la flexibilidad que otorga es inmensa.


5. Multi-tenancy y aislamiento: El Zen de Python en la infraestructura

Entrevistador: Mencionaste que el sistema no era solo para tus tiendas, sino que sumaste clientes comerciales externos. ¿Cómo resolviste la arquitectura multi-inquilino sin mezclar datos?

Ruperto: Aplicando el principio de “Simple es mejor que complejo”:

  1. Contenedor aislado por cliente: Cada empresa tiene su propia instancia de FactuKora corriendo en Docker dentro del servidor Ubuntu.
  2. Subdominios y Reverse Proxy: NGINX recibe las peticiones (empresaA.factukora.net, empresaB.factukora.net) y las despacha al puerto específico del contenedor correspondiente.
  3. Control de acceso en Tailscale: En Tailscale separé el acceso por nodos y tags. El servidor de la aplicación pertenece a los grupos de cada empresa, pero el SQL Server de la Empresa A jamás tiene visibilidad ni acceso a la red de la Empresa B.

6. Dolores reales de trinchera y evolución operativa

Entrevistador: Durante el desarrollo y puesta en marcha, ¿cuál fue la fricción técnica más pesada con la que te topaste?

Ruperto: La etapa de experimentación inicial fue dura hasta dar con las piezas correctas. Probé diferentes conectores y engines de base de datos para conectar Django con MSSQL (mssql-django, pyodbc, drivers de ODBC en Linux) y tuve que reconstruir el proyecto varias veces.

En la infraestructura también hubo evolución: al principio intenté manejar todo con múltiples archivos docker-compose-negocio1.yml, docker-compose-negocio2.yml. Con el tiempo descubrí que lo más limpio y mantenible era tener una estructura de carpetas aislada:

~/docker/
├── factukora-negocio1/
│   ├── docker-compose.yml
│   └── .env
└── factukora-negocio2/
    ├── docker-compose.yml
    └── .env

Comparten la misma imagen base, pero tienen contenedores, volúmenes y ciclos de vida independientes. Si necesito actualizar a un cliente o hacer mantenimiento, lo hago de forma aislada sin poner en riesgo a los demás.


7. Consejos para quien enfrenta un “dinosaurio” de escritorio

Entrevistador: Para cerrar: ¿qué le aconsejás a un desarrollador o equipo que hoy tiene un sistema legacy de escritorio y debe encarar una migración a la web?

Ruperto:

  1. Sé transparente desde el día uno: Cuando inicié la migración fui completamente honesto con los clientes. Les dije: «Me voy a meter en un hoyo negro del que no sé todo de antemano; necesito tiempo para explorar y experimentar». La confianza y la claridad eliminan la presión de tener que entregar soluciones mágicas e improvisadas.
  2. Cuidado sagrado con los datos: Nunca hagas pruebas en producción. Sacá una copia completa de la base de datos del cliente, montala en tu entorno local y rompela todas las veces que sea necesario.
  3. Respaldo diario multirregión/multidestino: En producción, respaldá diariamente la base de datos en al menos dos destinos diferentes. Ante cualquier imprevisto, poder restaurar el negocio en minutos te da la tranquilidad necesaria para seguir innovando.